﻿package com.moonshineproject.plugin.actionscript.fdb
{
	import com.moonshineproject.IDEModel;
	import com.moonshineproject.editor.BasicTextEditor;
	import com.moonshineproject.events.CloseTabEvent;
	import com.moonshineproject.events.EditorPluginEvent;
	import com.moonshineproject.events.OpenFileEvent;
	import com.moonshineproject.plugin.IMenuPlugin;
	import com.moonshineproject.plugin.IPlugin;
	import com.moonshineproject.plugin.PluginBase;
	import com.moonshineproject.plugin.actionscript.as3project.vo.AS3ProjectVO;
	import com.moonshineproject.plugin.actionscript.fdb.event.FDBEvent;
	import com.moonshineproject.plugin.actionscript.fdb.view.FDBView;
	import com.moonshineproject.plugin.actionscript.mxmlc.MXMLCPluginEvent;
	import com.moonshineproject.plugin.core.compiler.CompilerEventBase;
	import com.moonshineproject.plugin.menu.MenuPlugin;
	import com.moonshineproject.plugin.menu.vo.MenuItem;
	import com.moonshineproject.plugin.project.vo.ProjectVO;
	import com.moonshineproject.vo.Settings;
	
	import flash.desktop.NativeApplication;
	import flash.desktop.NativeProcess;
	import flash.desktop.NativeProcessStartupInfo;
	import flash.events.Event;
	import flash.events.NativeProcessExitEvent;
	import flash.events.ProgressEvent;
	import flash.filesystem.File;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	
	import mx.collections.XMLListCollection;
	import mx.core.FlexGlobals;
	import mx.events.ListEvent;
	import mx.events.TreeEvent;
	
	public class FDBPlugin extends PluginBase implements IPlugin, IMenuPlugin
	{
		override public function get name():String 			{ return "Flex Debugger Plugin"; }
		override public function get author():String 		{ return "Miha Lunar & Moonshine Project Team"; }
		override public function get description():String 	{ return "Debugs AS3 projects with FDB."; }
		
		private static const CONSOLE_MODE:String = "fdb";
		
		private var fdbPath:String      = "bin/fdb";
		private var outputBuffer:String = "";
		
		private var cookie:Object;
		private var currentSDK:File;
		private var debuggerInfo:NativeProcessStartupInfo;
		private var fdb:NativeProcess;
		
		private var manualMode:Boolean     = false;
		private var localsNext:Boolean     = false;
		private var itemQueue:Vector.<XML> = new Vector.<XML>();
		
		private var debugView:FDBView;
		private var objectTree:XMLListCollection;
		
		public function FDBPlugin()
		{
			if (Settings.os == "win")
				fdbPath += ".exe";
		}
		
		override public function activate():void
		{
			super.activate();
			
			debugView = new FDBView();
			debugView.addEventListener(TreeEvent.ITEM_OPEN, objectOpened);
			debugView.addEventListener(ListEvent.ITEM_DOUBLE_CLICK, refreshItem);
			objectTree = debugView.objectTree;
			
			//IDEModel.getInstance().mainView.addPanel(debugView);
			
			dispatcher.addEventListener(CompilerEventBase.BUILD_AND_DEBUG, prebuild);
			dispatcher.addEventListener(CompilerEventBase.PREBUILD, handleCompile);
			//dispatcher.addEventListener(CompilerEventBase.POSTBUILD, startDebugView);
			dispatcher.addEventListener(EditorPluginEvent.EVENT_EDITOR_OPEN, handleEditorOpen);
			dispatcher.addEventListener(MenuPlugin.MENU_SAVE_EVENT, handleEditorSave);
			dispatcher.addEventListener(MenuPlugin.MENU_SAVE_AS_EVENT, handleEditorSave);
			dispatcher.addEventListener(CloseTabEvent.EVENT_CLOSE_TAB, handleEditorSave);
			dispatcher.addEventListener(FDBEvent.SHOW_DEBUG_VIEW, handleShowDebugView);
			
			registerCommand(CONSOLE_MODE, fdbCommand);
			
			cookie = {};
		}
		
		private function handleShowDebugView(e:Event):void
		{
			IDEModel.getInstance().mainView.addPanel(debugView);
		}
		
		public function getMenu():MenuItem
		{
			var viewMenu : MenuItem = new MenuItem("");
			viewMenu.parents = ["View"];
			viewMenu.items = new Vector.<MenuItem>();
			viewMenu.items.push(new MenuItem('Debug view', null, FDBEvent.SHOW_DEBUG_VIEW));
			return viewMenu;
		}
		
		private function objectOpened(e:TreeEvent):void
		{
			var item : XML = XML(e.item);
			if (item.children().length() == 0)
			{
				updateItem(item);
			}
		}
		
		private function refreshItem(e:ListEvent):void
		{
			var item : XML = XML(e.itemRenderer.data);
			updateItem(item);
		}
		
		private function updateItem(item:XML):void
		{
			item.@label = "updating...";
			itemQueue.push(item);
			send("print " + item.@path + (item.@isBranch == "true" ? "." : ""));
		}
		
		override public function deactivate():void
		{
			super.deactivate();
			dispatcher.removeEventListener(CompilerEventBase.PREBUILD, prebuild);
			
			unregisterCommand(CONSOLE_MODE);
			
			if (fdb)
				fdb.exit();
		}
		
		private function handleEditorOpen(event:EditorPluginEvent):void
		{
			if (event.newFile || !event.file)
				return;
			
			var path : String = event.file.nativePath;
			var breakpoints : Array = cookie[path] as Array;
			if (breakpoints)
			{
				event.editor.breakpoints = breakpoints;
			}
		}
		
		private function handleEditorSave(event:Event):void
		{
			var editor : BasicTextEditor;
			if (event is CloseTabEvent)
			{
				editor = CloseTabEvent(event).tab as BasicTextEditor;
			}
			else
			{
				editor = IDEModel.getInstance().activeEditor as BasicTextEditor;
			}
			
			saveForEditor(editor);
		}
		
		private function handleCompile(event:MXMLCPluginEvent):void
		{
			currentSDK = event.sdk;
			// Make sure we have stuff from all editors. (even unsaved?)
			var editors : Array = IDEModel.getInstance().editors.source;
			for (var i : int = 0; i < editors.length; i++)
			{
				saveForEditor(editors[i] as BasicTextEditor);
			}
		}
		
		private function saveForEditor(editor:BasicTextEditor):void
		{
			if (!editor)
				return;
			if (!editor.currentFile)
				return;
			
			var path : String = editor.currentFile.nativePath;
			if (path == "")
				return;
			
			cookie[path] = editor.getEditorComponent().breakpoints;
		}
		
		private function fdbCommand(args:Array):void
		{
			if (args.length == 0)
			{
				enterConsoleMode(CONSOLE_MODE);
				manualMode = true;
			}
			else if (args[0] == "exit")
			{
				exitConsoleMode();
				manualMode = false;
				if (fdb)
					send(args.join(" "));
			}
			else
			{
				print("FDB " + args.join(" "));
				if (!fdb)
				{
					print("FDB not running, please build the project you want to debug at least once.");
				}
				else
				{
					send(args.join(" "));
				}
			}
		}
		
		private function initDebugger():void
		{
			if (!currentSDK) 
			{
				error("No Flex SDK set, check settings.");
				return;
			}
			
			debuggerInfo = new NativeProcessStartupInfo();
			debuggerInfo.executable = currentSDK.resolvePath(fdbPath);
			if (model.activeProject)
			{
				debuggerInfo.workingDirectory = model.activeProject.folder;
			}
			
			fdb = new NativeProcess();
			fdb.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA, debuggerData);
			fdb.addEventListener(ProgressEvent.STANDARD_ERROR_DATA, debuggerError);
			fdb.addEventListener(NativeProcessExitEvent.EXIT, debuggerExit);
			fdb.start(debuggerInfo);
		}
		
		private function getMainTargetFolder():File
		{
			var project : AS3ProjectVO = AS3ProjectVO(IDEModel.getInstance().activeProject);
			return project.targets.length == 0 ? null : project.targets[0].parent;
		}
		
		/**
		 * Returns the file path relative to the project's main target path
		 */
		private function getRelativeTargetPath(f:File):String
		{
			return getMainTargetFolder().getRelativePath(f, true);
		}
		
		/**
		 * Resolves the path based on the project's main target path
		 */
		private function resolveTargetPath(path:String):File
		{
			return getMainTargetFolder().resolvePath(path);
		}
		
		private function sessionStart():void
		{
			send("delete");
			send("y");
			for (var path : String in cookie)
			{
				var f : File = new File(path);
				send("cf " + getRelativeTargetPath(f));
				var breakpoints : Array = cookie[path];
				for (var i : int = 0; i < breakpoints.length; i++)
				{
					send("break " + (breakpoints[i] + 1));
				}
			}
			if (!manualMode)
				send("continue");
			
			// Add debugview if not visible
			if (!debugView.stage) model.mainView.addPanel(debugView);
		}
		
		private function sessionStop():void
		{
			if (debugView.stage) debugView.parent.removeChild(debugView);
		}
		
		private function debuggerData(e:ProgressEvent):void
		{
			var output:IDataInput = fdb.standardOutput;
			var data:String = output.readUTFBytes(output.bytesAvailable);
			
			var match:Array;
			
			var project:ProjectVO = IDEModel.getInstance().activeProject;
			
			match = data.match(/.*Player connected; session starting\..*/);
			if (match)
			{
				sessionStart();
			}
			
			match = data.match(/.*Player session terminated.*/);
			if (match)
			{
				sessionStop();
			}
			
			match = data.match(/\[trace\] (.*)\n/s);
			if (match)
			{
				print(match[1]);
			}
			else
			{
				outputBuffer += data;
			}
			
			
			//if (data.charAt(data.length-1) == "\n") data = data.substr(0, data.length-1);
			
			match = outputBuffer.match(/(.*)\n\(fdb\) /s);
			if (match)
			{
				var buffer:String = match[1];
				
				if (manualMode)
				{
					print("fdb> " + buffer);
				}
				else if (itemQueue.length > 0)
				{
					var item:XML = itemQueue.shift();
					
					var branch:Boolean = item.@isBranch == "true";
					// Remove all items first
					if (branch)
						item.setChildren(new XMLList());
					
					var objects:Array = buffer.replace(/\r\n/g, "\n").split("\n");
					var skipFirst:Boolean = !localsNext && branch;
					for each (var objLine:String in objects)
					{
						if (skipFirst)
						{
							skipFirst = false;
							continue;
						}
						match = objLine.match(" ?(.*?) = (.*)");
						if (!match)
							continue;
						var objName:String = match[1];
						var objValue:String = match[2];
						//ConsoleUtil.print("ITEM " + objLine + " # " + match.length + " ### " + objName + " ### " + objValue);
						if (branch)
						{
							var complex:Boolean = Boolean(objValue.match(/^\[Object .*?\]$/));
							var newItem:XML = <item label={objName + "\t " + objValue} path={item.@path + (item.@path == "" ? "" : ".") + objName} name={objName} value={objValue}/>;
							if (complex)
								newItem.@isBranch = "true";
							item.appendChild(newItem);
						}
						else
						{
							item.@value = objValue;
						}
					}
					item.@label = item.@name + "\t " + item.@value;
					
					localsNext = false;
				}
				
				match = buffer.match(/^\[SWF\].*?Additional ActionScript code has been loaded from a SWF or a frame/s);
				if (match) {
					send("continue");
				}
				
				match = buffer.match(/Breakpoint (\d*?), (.*?) at (.*?):(\d*).*?/);
				if (match)
				{
					var bpNum:int = int(match[1]);
					var bpFunc:String = match[2];
					var bpFile:String = match[3];
					var bpLine:int = int(match[4]) - 1;
					
					// Open file & scroll & select the given line
					dispatcher.dispatchEvent(
						new OpenFileEvent(OpenFileEvent.OPEN_FILE, resolveTargetPath(bpFile), bpLine)
					);
					
					// Chances are we're not in focus here, so let's focus Moonshine
					// This slows everything down like /crazy/. Why?
					//NativeApplication.nativeApplication.activate(NativeApplication.nativeApplication.openedWindows[0]);
					
					print("Breakpoint in " + bpFunc + " at line " + (bpLine+1) + " of " + bpFile);
					if (!manualMode)
					{
						send("info locals");
						objectTree.removeAll();
						objectTree.addItem(<item label="this" path="this" name="this" value="" isBranch="true" />);var itemLocals : XML =                <item label="locals" path="" name="locals" value="" isBranch="true" />;
						objectTree.addItem(itemLocals);
						debugView.expandItem(itemLocals);
						itemQueue.push(itemLocals);
						localsNext = true;
						outputBuffer = "";
					}
				}
				
				outputBuffer = "";
			}
			
			debug(">>> %s <<<", data);
		}
		
		private function debuggerError(e:ProgressEvent):void
		{
			var output:IDataInput = fdb.standardOutput;
			var data:String = output.readUTFBytes(output.bytesAvailable);
			
			if (data.charAt(data.length - 1) == "\n")
				data = data.substr(0, data.length - 1);
			if (manualMode)
			{
				print("fdb> " + data);
			}
			
			debug("Error: %s", data); 
		}
		
		private function debuggerExit(e:NativeProcessExitEvent):void
		{
			debug("FDB exit code %s", e.exitCode);
			fdb = null;
		}
		
		private function prebuild(e:Event):void
		{
			// In case we have no SDK set we fail silently (MXMLCPlugin will be all over it).
			if (!currentSDK)
			{
				return;
			}
			
			if (!fdb)
			{
				initDebugger();
			}
			send("run");
		}
		
		private function send(msg:String):void
		{
			debug("Send to fdb: %s", msg);
			var input:IDataOutput = fdb.standardInput;
			input.writeUTFBytes(msg + "\n");
		}
	}
}